/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.client;

import java.util.List;

import org.cometmi.gwt.client.impl.ProxyFactoryClient;
import org.cometmi.gwt.client.impl.WrapperFactoryClient;
import org.cometmi.gwt.shared.MiQueue;
import org.cometmi.gwt.shared.MiQueuePool;
import org.cometmi.gwt.shared.events.AttributesCallback;
import org.cometmi.gwt.shared.events.BindCallback;
import org.cometmi.gwt.shared.events.PublishCallback;
import org.cometmi.gwt.shared.exceptions.NameParseException;
import org.cometmi.gwt.shared.exceptions.NotAllowedQueueException;
import org.cometmi.gwt.shared.request.Call;
import org.cometmi.gwt.shared.request.FreeRequestBean;
import org.cometmi.gwt.shared.request.ListenRequestBean;
import org.cometmi.gwt.shared.request.PublishRequestBean;
import org.cometmi.gwt.shared.request.QueueRequestBean;
import org.cometmi.gwt.shared.request.ResponseStatus;
import org.cometmi.gwt.shared.util.Constants;
import org.cometmi.gwt.shared.util.ProxyFactory;
import org.cometmi.gwt.shared.util.UtilityFunctions;
import org.cometmi.gwt.shared.util.WrapperFactory;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;

/**
 * Main responsible object to perform requests.
 * 
 * @author marco.tamburelli@gmail.com
 */
public class Connection
{
	public static Connection INSTANCE = new Connection();

	protected final CometMiServiceAsync cometMiService = GWT.create(CometMiService.class);

	protected final ProxyFactory proxyFactory = GWT.create(ProxyFactoryClient.class);
	protected final WrapperFactory wrapperFactory = GWT.create(WrapperFactoryClient.class);

	private PublishCallback publishCallback;

	private String userCredentials;

	private class MiCallBack implements AsyncCallback<List<Call>>
	{
		private MiQueueClient queue;
		private ListenRequestBean request;
		private BindCallback callback;

		MiCallBack(MiQueueClient queue, ListenRequestBean request, BindCallback callback)
		{
			this.queue = queue;
			this.request = request;
			this.callback = callback;
		}

		@Override
		public void onSuccess(List<Call> result)
		{
			try
			{
				this.queue.onResponse(result);

				if (this.queue.isDisconnecting())
					this.queue.forceDisconnect();
				else
					Connection.this.cometMiService.listen(this.request, this);
			}
			catch (Exception e)
			{
				this.callback.onError(e);
				this.queue.polling = false;
			}
		}

		@Override
		public void onFailure(final Throwable caught)
		{
			/*
			 * However the service finish, the system always attempt to
			 * reconnect.
			 */
			Connection.this.cometMiService.checkConnection(new AsyncCallback<Void>()
			{
				@Override
				public void onSuccess(Void result)
				{
					Connection.this.cometMiService.listen(MiCallBack.this.request, MiCallBack.this);
				}

				@Override
				public void onFailure(Throwable caught1)
				{
					MiCallBack.this.callback.onError(caught1);
					MiCallBack.this.queue.polling = false;
					MiCallBack.this.queue.forceDisconnect();
				}
			});
		}
	};

	private Connection()
	{
		/* The default constructor ensure the proper service end point */
		((ServiceDefTarget) this.cometMiService).setServiceEntryPoint(Constants.COMETMI_BASE_URL
				+ Constants.SERVICE_ENDPOINT);

		this.userCredentials = "random generated " + UtilityFunctions.generateUniqueId();
		this.publishCallback = new PublishCallback()
		{
			@Override
			public void onError(Throwable t)
			{
				GWT.log("Error while publishing: ", t);
			}
		};
	}

	/**
	 * This method register the user credentials for authentication.
	 * 
	 * @param credentials
	 */
	public void provideCredentials(String credentials)
	{
		this.userCredentials = credentials;
	}

	/**
	 * Add a callback class, which will fire it's
	 * {@link PublishCallback#onError(Throwable)} method in case of error. Of
	 * course the callback is not required, it should be provided in case the
	 * program requires to control the status of published elements.
	 * 
	 * @param callback Callback object.
	 */
	public void addPublishCallback(PublishCallback callback)
	{
		this.publishCallback = callback;
	}

	/**
	 * Returns a {@link MiQueue} implementation suitable for client management.
	 * 
	 * @param queueName The name of required queue.
	 * @return The requested queue.
	 * @throws NameParseException
	 */
	public MiQueue getQueue(String queueName) throws NameParseException
	{
		if (!queueName.matches("((\\w+)\\.)*(\\w+)")) throw new NameParseException("Invalid queue name: " + queueName);

		MiQueueClient q = new MiQueueClient(queueName, this.proxyFactory, this.wrapperFactory);

		return q;
	}

	/**
	 * Returns a {@link MiQueuePool} implementation suitable for client
	 * management.
	 * 
	 * @param queueExpr The expression of the set of requested queues. Note that
	 *        the expression must identify a pool: must be of the form
	 *        <code>org.test.*</code>
	 * @return Teh requested queue pool.
	 * @throws NameParseException
	 */
	public MiQueuePool getQueuePool(String queueExpr) throws NameParseException
	{
		if (!queueExpr.matches("((\\w+)\\.)*(\\*)"))
			throw new NameParseException("Invalid queue expression: " + queueExpr);

		MiQueuePoolClient q = new MiQueuePoolClient(queueExpr, this.proxyFactory);

		return q;
	}

	/**
	 * This method publishes the provided array of calls.
	 * 
	 * @param queueExpression
	 * @param calls
	 */
	void publish(String queueExpression, List<Call> calls)
	{
		PublishRequestBean publishRequest = new PublishRequestBean();
		publishRequest.setCredentials(this.userCredentials);
		publishRequest.setCalls(calls);
		publishRequest.setQueueExpression(queueExpression);

		this.cometMiService.publish(publishRequest, new AsyncCallback<ResponseStatus>()
		{
			@Override
			public void onSuccess(ResponseStatus result)
			{
				if (result != ResponseStatus.Success)
					Connection.this.publishCallback.onError(new NotAllowedQueueException());
				else
					GWT.log("Published request...");
			}

			@Override
			public void onFailure(Throwable caught)
			{
				Connection.this.publishCallback.onError(caught);
			}
		});
	}

	void listen(final MiQueueClient queue, final BindCallback callback)
	{
		QueueRequestBean queueRequest = new QueueRequestBean();
		queueRequest.setQueueName(queue.getExpression());
		queueRequest.setCredentials(this.userCredentials);
		queueRequest.setAttributes(queue.getAttributes());

		this.cometMiService.checkQueueAvailability(queueRequest, new AsyncCallback<ResponseStatus>()
		{
			@Override
			public void onSuccess(ResponseStatus result)
			{
				switch (result)
				{
				case InvalidCredentials:
					// TODO
					break;
				case QueueAlreadyBound:
					callback.onError(new NotAllowedQueueException("The queue " + queue.getExpression()
							+ " is not available because already in use."));
					break;
				case Success:
					queue.polling = true;

					ListenRequestBean request = new ListenRequestBean();
					request.setQueueName(queue.getExpression());
					request.setCredentials(Connection.this.userCredentials);

					Connection.this.cometMiService.listen(request, new MiCallBack(queue, request, callback));
					callback.onSuccess();
					break;
				}
			}

			@Override
			public void onFailure(Throwable caught)
			{
				callback.onError(caught);
				queue.forceDisconnect();
			}
		});
	}

	void setQueueAttribute(String queueName, String key, String value, final AttributesCallback callback)
	{
		this.cometMiService.setQueueAttribute(queueName, key, value, new AsyncCallback<Void>()
		{
			@Override
			public void onSuccess(Void result)
			{}

			@Override
			public void onFailure(Throwable caught)
			{
				callback.onError(caught);
			}
		});
	}

	void getQueueAttribute(String queueName, String key, final AttributesCallback callback)
	{
		this.cometMiService.getQueueAttribute(queueName, key, new AsyncCallback<String>()
		{
			@Override
			public void onSuccess(String result)
			{
				callback.onSuccess(result);
			}

			@Override
			public void onFailure(Throwable caught)
			{
				callback.onError(caught);
			}
		});
	}

	void removeQueueAttribute(String queueName, String key, final AttributesCallback callback)
	{
		this.cometMiService.removeQueueAttribute(queueName, key, new AsyncCallback<String>()
		{
			@Override
			public void onSuccess(String result)
			{
				callback.onSuccess(result);
			}

			@Override
			public void onFailure(Throwable caught)
			{
				callback.onError(caught);
			}
		});
	}

	void dispose(final MiQueueClient queue, final BindCallback callback, boolean discard)
	{
		FreeRequestBean disposeRequest = new FreeRequestBean();
		disposeRequest.setQueueName(queue.getExpression());
		disposeRequest.setCredentials(this.userCredentials);
		disposeRequest.setDiscard(discard);

		this.cometMiService.freeQueue(disposeRequest, new AsyncCallback<Void>()
		{
			@Override
			public void onSuccess(Void result)
			{
				callback.onSuccess();
			}

			@Override
			public void onFailure(Throwable caught)
			{
				callback.onError(caught);
			}
		});
	}
}
